package fidl.tool;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.HashSet;

import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.resource.Resource;
import org.franca.core.franca.FAnnotation;
import org.franca.core.franca.FAnnotationBlock;
import org.franca.core.franca.FArgument;
import org.franca.core.franca.FArrayType;
import org.franca.core.franca.FAttribute;
import org.franca.core.franca.FBasicTypeId;
import org.franca.core.franca.FBroadcast;
import org.franca.core.franca.FConstantDef;
import org.franca.core.franca.FEnumerationType;
import org.franca.core.franca.FEnumerator;
import org.franca.core.franca.FField;
import org.franca.core.franca.FInterface;
import org.franca.core.franca.FMapType;
import org.franca.core.franca.FModel;
import org.franca.core.franca.FModelElement;
import org.franca.core.franca.FStructType;
import org.franca.core.franca.FType;
import org.franca.core.franca.FTypeCollection;
import org.franca.core.franca.FTypeDef;
import org.franca.core.franca.FTypeRef;

public class GeneratorProto {
	public boolean treatEnumAsInt = false;
	public boolean treatTypedefAsMessage = false;
	public String prefixNameSpace = "";

	FModel model = null;
	FTypeCollection container = null;
	HashSet<FModel> importModels = new HashSet<FModel>();

	boolean doGenerate(Resource resource, String outPath) {
		if (resource.getContents().isEmpty()) {
			System.out.println("An empty file: " + resource.getURI().path());
			return false;
		}
		EObject root = resource.getContents().get(0);
		if (root instanceof FModel) {
			model = (FModel) root;
		} else {
			System.out.println("Unsupport file: " + resource.getURI().path());
			return false;
		}
		// generate body first, because of we need know which types is import.
		importModels.clear();
		StringBuilder body = new StringBuilder();
		for (FTypeCollection typeCollection : model.getTypeCollections()) {
			generate(body, typeCollection);
		}
		for (FInterface interface_ : model.getInterfaces()) {
			generate(body, interface_);
		}
		// generate head.
		StringBuilder head = new StringBuilder();
		head.append("// Generated by FrancaIDL tool, Do NOT Modify!\n\n");
		head.append("syntax = \"proto3\";\n\n");
		head.append("package ");
		String fullNameSpace;
		if (prefixNameSpace.isEmpty()) {
			fullNameSpace = model.getName();
		} else {
			fullNameSpace = prefixNameSpace + "." + model.getName();
		}
		head.append(fullNameSpace);
		head.append(";\n\n");
		for (FModel importModel : importModels) {
			generateImport(head, importModel);
		}
		head.append("\n");
		// write to file.
		FileWriter writer = null;
		BufferedWriter bufferedWriter = null;
		try {
			StringBuilder filename = new StringBuilder();
			filename.append(outPath);
			String[] namespaces = fullNameSpace.split("\\.");
			for (String namespace : namespaces) {
				filename.append("/" + namespace);
			}
			Cli.checkPath(filename.toString());
			filename.append("/");
			filename.append(Cli.getResourceFileName(resource));
			filename.append(".proto");
			File file = new File(filename.toString());
			writer = new FileWriter(file);
			bufferedWriter = new BufferedWriter(writer);
			bufferedWriter.write(head.toString());
			bufferedWriter.write(body.toString());
			bufferedWriter.close();
		} catch (IOException e) {
			e.printStackTrace();
			return false;
		}
		return true;
	}

	boolean generate(StringBuilder builder, FTypeCollection typeCollection) {
		container = typeCollection;
		String name = container.getName();
		if (container instanceof FInterface) {
			if (name == null) {
				System.out.println("Interface name can not be null.");
				return false;
			}
			builder.append("message " + name + "\n{\n");
		} else if (name != null) {
			builder.append("message " + name + " {\n\n");
		}
		for (FType type : container.getTypes()) {
			generateType(builder, type);
		}
		for (FConstantDef constant : container.getConstants()) {
			generateConstant(builder, constant);
		}
		if (container instanceof FInterface) {
			FInterface interface_ = (FInterface) container;
			int i = 1;
			for (FAttribute attribute : interface_.getAttributes()) {
				generateAttribute(builder, attribute);
				builder.append(" = " + i + ";\n");
				i++;
			}
			for (FBroadcast broadcast : interface_.getBroadcasts()) {
				generateBroadcast(builder, broadcast);
			}
			builder.append("} // " + interface_.getName() + "\n");
		} else if (name != null) {
			builder.append("} // " + container.getName() + "\n");
		}
		return true;
	}

	void generateBroadcast(StringBuilder builder, FBroadcast broadcast) {
		generateCommet(builder, broadcast.getComment());
		builder.append("message " + broadcast.getName() + "\n{\n");
		int i = 1;
		for (FArgument argument : broadcast.getOutArgs()) {
			generateCommet(builder, argument.getComment());
			FTypeRef typeRef = argument.getType();
			builder.append("\t" + resovleTypeName(typeRef));
			builder.append(" " + argument.getName());
			builder.append(" = " + i + ";\n");
			i++;
		}
		builder.append("}\n\n");
	}

	void generateAttribute(StringBuilder builder, FAttribute attribute) {
		generateCommet(builder, attribute.getComment());
		FTypeRef typeRef = attribute.getType();
		builder.append(resovleTypeName(typeRef));
		builder.append(" " + attribute.getName());
	}

	void generateConstant(StringBuilder builder, FConstantDef constantDef) {
		generateCommet(builder, constantDef.getComment());
		FTypeRef typeRef = constantDef.getType();
		builder.append("#error I dont know what it is, so I dont deal it.");
		builder.append(resovleTypeName(typeRef));
		builder.append(" " + constantDef.getName());
		builder.append(constantDef.getRhs().toString());
	}

	void generateType(StringBuilder builder, FType type) {
		generateCommet(builder, type.getComment());
		if (type instanceof FStructType) {
			FStructType struct = (FStructType) type;
			builder.append("message " + type.getName() + "\n{\n");
			int i = 1;
			for (FField member : struct.getElements()) {
				generateCommet(builder, member.getComment());
				FTypeRef typeRef = member.getType();
				if (member.isArray()) {
					builder.append("\trepeated ");
					builder.append(resovleTypeName(typeRef));
				} else {
					builder.append("\t" + resovleTypeName(typeRef));
				}
				builder.append(" " + member.getName());
				builder.append(" = " + i + ";\n");
				i++;
			}
			builder.append("}\n\n");
		} else if ((type instanceof FTypeDef) && treatTypedefAsMessage) {
			FTypeDef typeDef = (FTypeDef) type;
			FTypeRef typeRef = typeDef.getActualType();
			builder.append("message " + typeDef.getName() + "\n{\n\t");
			builder.append(resovleTypeName(typeRef) + " value = 1;\n}\n");
		} else if ((type instanceof FMapType) && treatTypedefAsMessage) {
			FMapType mapType = (FMapType) type;
			FTypeRef keyType = mapType.getKeyType();
			FTypeRef valueType = mapType.getValueType();
			builder.append("message " + mapType.getName() + "\n{\n\t");
			builder.append("map<" + resovleTypeName(keyType));
			builder.append(", " + resovleTypeName(valueType));
			builder.append("> value = 1;\n}\n");
		} else if ((type instanceof FArrayType) && treatTypedefAsMessage) {
			FArrayType arrayType = (FArrayType) type;
			FTypeRef eleType = arrayType.getElementType();
			builder.append("message " + arrayType.getName() + "\n{\n\t");
			builder.append("repeated " + resovleTypeName(eleType));
			builder.append(" value = 1;\n}\n");
		} else if ((type instanceof FEnumerationType) && !treatEnumAsInt) {
			builder.append("enum " + type.getName() + "\n{\n");
			FEnumerationType enumeration = (FEnumerationType) type;
			int i = 0;
			for (FEnumerator enumerator : enumeration.getEnumerators()) {
				generateCommet(builder, enumerator.getComment());
				builder.append("\t" + enumerator.getName());
				builder.append(" = " + i + ";\n");
				i++;
			}
			builder.append("}\n\n");
		}
	}

	void generateCommet(StringBuilder builder, FAnnotationBlock annotation) {
		if (annotation == null) {
			return;
		}
		EList<FAnnotation> commets = annotation.getElements();
		for (FAnnotation commet : commets) {
			String[] lines = commet.getComment().split("\n");
			for (String line : lines) {
				builder.append("// " + line.trim() + "\n");
			}
		}
	}

	void generateImport(StringBuilder builder, FModel importModel) {
		builder.append("import \"");
		String fullNameSpace = "";
		if (prefixNameSpace.isEmpty()) {
			fullNameSpace = importModel.getName();
		} else {
			fullNameSpace = prefixNameSpace + "." + importModel.getName();
		}
		builder.append(fullNameSpace.replace('.', '/'));
		builder.append("/");
		builder.append(Cli.getResourceFileName(importModel.eResource()));
		builder.append(".proto\";\n");
	}

	String resovleTypeName(FTypeRef type) {
		FBasicTypeId id = type.getPredefined();
		switch (id.getValue()) {
		case FBasicTypeId.BOOLEAN_VALUE:
			return "bool";
		case FBasicTypeId.INT8_VALUE:
		case FBasicTypeId.INT16_VALUE:
		case FBasicTypeId.INT32_VALUE:
			return "int32";
		case FBasicTypeId.INT64_VALUE:
			return "int64";
		case FBasicTypeId.UINT8_VALUE:
		case FBasicTypeId.UINT16_VALUE:
		case FBasicTypeId.UINT32_VALUE:
			return "uint32";
		case FBasicTypeId.UINT64_VALUE:
			return "uint64";
		case FBasicTypeId.FLOAT_VALUE:
			return "float";
		case FBasicTypeId.DOUBLE_VALUE:
			return "double";
		case FBasicTypeId.STRING_VALUE:
		case FBasicTypeId.BYTE_BUFFER_VALUE:
			return "bytes";
		default:
			break;
		}
		FModelElement baseType = type.getDerived();
		if ((baseType instanceof FTypeDef) && !treatTypedefAsMessage) {
			FTypeDef typeDef = (FTypeDef) baseType;
			FTypeRef typeRef = typeDef.getActualType();
			return resovleTypeName(typeRef);
		}
		if ((baseType instanceof FMapType) && !treatTypedefAsMessage) {
			FMapType mapType = (FMapType) baseType;
			FTypeRef keyType = mapType.getKeyType();
			FTypeRef valueType = mapType.getValueType();
			return "map<" + resovleTypeName(keyType) + ", "
					+ resovleTypeName(valueType) + ">";
		}
		if ((baseType instanceof FArrayType) && !treatTypedefAsMessage) {
			FArrayType arrayType = (FArrayType) baseType;
			FTypeRef eleType = arrayType.getElementType();
			return "repeated " + resovleTypeName(eleType);
		}
		if ((baseType instanceof FEnumerationType) && treatEnumAsInt) {
			return "int32";
		}
		if (baseType != null) {
			String fullName = baseType.getName();
			while (true) {
				EObject object = baseType.eContainer();
				if (container == object) {
					break;
				}
				if (object instanceof FModelElement) {
					baseType = (FModelElement) object;
					String typeName = baseType.getName();
					if (typeName != null) {
						fullName = typeName + "." + fullName;
					}
				} else if (object instanceof FModel) {
					FModel containerModel = (FModel) object;
					if (containerModel != model) {
						fullName = containerModel.getName() + "." + fullName;
						importModels.add(containerModel);
					}
					break;
				} else {
					return "undefined";
				}
			}
			return fullName;
		}
		return "undefined";
	}
}
